﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Data;
using System.Data.Common;
using System.Data.SqlClient;

namespace AcademicPlanner.Framework.Libraries.Auth
{
	/// <summary>
	/// List of reasons why the authorization requested
	/// was granted or denied.
	/// </summary>
	/// <version>1.0</version>
	/// <since>1.0</since>
	public enum RnRBAuthorizationResultReason
	{
		NO_USER_ROLES,
		NO_USER_ROLES_ASSIGNED_OPERATION,
		USER_ROLES_ASSIGNED_OPERATION,
		NO_REQUIRED_RELATIONSHIPS,
		NO_REQUIRED_RELATIONSHIP_FOR_USER_ROLES,
		REQUIRED_RELATIONSHIPS_FOR_USER_ROLES,
		USER_RELATED_TO_OBJECT_OWNER,
	}

	/// <summary>
	/// Represents the result of an authorization request
	/// for a Relationship and Role Based Authorization policy.
	/// </summary>
	/// <version>1.0</version>
	/// <since>1.0</since>
	public class RnRBAuthorizationResult : AuthorizationResult
	{
		/// <summary>
		/// The reason for the outcome of the requested authorization.
		/// </summary>
		/// <version>1.0</version>
		/// <since>1.0</since>
		internal RnRBAuthorizationResultReason reason;

		/// <summary>
		/// Gets the reason for the outcome of the requested authorization.
		/// </summary>
		/// <version>1.0</version>
		/// <since>1.0</since>
		public RnRBAuthorizationResultReason Reason { get { return reason; } }
	}

	/// <summary>
	/// RnRBAuthorizer provices authorization services for a 
	/// Relationship and Role Based Authorization policy. A 
	/// RnRBAuthorizer expects the authorization data to be stored
	/// in a SQL database.
	/// </summary>
	/// <version>1.0</version>
	/// <since>1.0</since>
	public class RnRBAuthorizer : IAuthorizer
	{
		private IDbConnection connection;

		private SqlDataAdapter commonDataAdapter;

		private string tableUsersName = "users";

		private string tableRolesName = "roles";

		private string tableUserRolesName = "user_roles";

		private string tableUserRelationshipsName = "user_relationships";

		private string tableOperationsName = "operations";

		private string tableRoleOperationsName = "role_operations";

		private string tableOperationRelationshipTypesName = "operation_relationship_types";

		private string tableRelationshipTypesName = "relationship_types";

		private string tableObjectsName = "objects";

		private string tableObjectTokensName = "object_tokens";


		/// <summary>
		/// Constructs a RnRBAuthorizer with the given connection string
		/// to the authorization rules data storage. 
		/// </summary>
		/// <param name="pConnection">The connection string to the authorization rules storage.</param>
		/// <version>1.0</version>
		/// <since>1.0</since>
		public RnRBAuthorizer(string pConnection)
		{
			connection = new SqlConnection(pConnection);
			commonDataAdapter = new SqlDataAdapter();
		}


		/// <summary>
		/// Gets the authorization details for a user to perform the given
		/// operation. This is a convenience overload of 
		/// <see cref="M:GetAuthorization(string, string, string)"/> with the
		/// third argument <c>null</c>.
		/// </summary>
		/// <param name="pUser">The user's identifier.</param>
		/// <param name="pOperation">The operation's identifier.</param>
		/// <returns>The <c>AuthorizationResult</c>.</returns>
		/// <version>1.0</version>
		/// <since>1.0</since>
		public AuthorizationResult GetAuthorization(string pUser, string pOperation)
		{
			return GetAuthorization(pUser, pOperation, null);
		}

		/// <summary>
		/// Gets the authorization details for a user to perform the given
		/// operation on the given object.
		/// This method first gets the roles assigned to the user and checks
		/// that at least one of those roles have been assigned the given
		/// operation. Then if <paramref name="pObject"/> is not <c>null</c>,
		/// it gets the list of relationships required between the user and the
		/// object's owner for the given operation when being performed under
		/// the roles it was assigned to. It then checks that for at least one
		///  of the user's roles having being assigned the operation, the user has
		/// the required relationship established with the object's owner.
		/// </summary>
		/// <param name="pUser">The user's identifier.</param>
		/// <param name="pOperation">The operation's identifier.</param>
		/// <param name="pObject">The object's identifier; may be <c>null</c>.</param>
		/// <returns>The <c>AuthorizationResult</c>.</returns>
		/// <version>1.0</version>
		/// <since>1.0</since>
		public AuthorizationResult GetAuthorization(string pUser, string pOperation, string pObject)
		{
			var rRes = new RnRBAuthorizationResult();
			var vUserRoles = GetUserRoles(pUser.ToLower()); // roles given to user

			rRes.reason = RnRBAuthorizationResultReason.NO_USER_ROLES;
			if (vUserRoles.Count() > 0)
			{
				var vQuery = from vRow in vUserRoles
							 let vRoleOps = GetRoleOperations(vRow.Field<int>("Role"))
							 where vRoleOps.Select(vRoleOp => vRoleOp.Field<string>("Operation").ToLower()).ToArray()
											.Contains(pOperation.ToLower())
							 select vRow;

				rRes.reason = RnRBAuthorizationResultReason.NO_USER_ROLES_ASSIGNED_OPERATION;
				if (pObject != null && vQuery.Count() > 0)
				{
					// get the relationships required between a user's role and the object of this operation
					var vOpRelationshipTypes = GetOperationRelationshipTypes(pOperation);

					rRes.reason = RnRBAuthorizationResultReason.NO_REQUIRED_RELATIONSHIPS;
					if (vOpRelationshipTypes.Count() > 0)
					{
						// get the intersection of the relationships required and the user's roles allowed for 
						// this operation
						var vQuery2 = from vRow in vOpRelationshipTypes
									  where vQuery.Select(vUserRole => vUserRole.Field<int>("Role")).ToArray()
													.Contains(vRow.Field<int>("Role"))
									  select vRow;

						rRes.reason = RnRBAuthorizationResultReason.NO_REQUIRED_RELATIONSHIP_FOR_USER_ROLES;
						if (vQuery2.Count() > 0)
						{
							var vObject = GetObject(pObject); // the ac object of the operation
							// get all the ac object's owner's relationships
							var vUserRelations = GetUserRelationships(vObject.Field<string>("User").ToLower());

							rRes.reason = RnRBAuthorizationResultReason.REQUIRED_RELATIONSHIPS_FOR_USER_ROLES;
							foreach (var vOpRel in vQuery2)
							{
								// check that the user is related in the required way with the ac
								// object's owner
								var vQuery3 = from vRow in vUserRelations
											  where vRow.Field<string>("RelatedUser").ToLower().Equals(pUser.ToLower())
													&& vRow.Field<int>("RelationshipType") == 
															vOpRel.Field<int>("RelationshipType")
											  select vRow;

								if (vQuery3.Count() > 0)
								{
									rRes.authorized = true;
									rRes.reason = RnRBAuthorizationResultReason.USER_RELATED_TO_OBJECT_OWNER;
									break;
								}
							}
						}
						else
							rRes.authorized = true;
					}
					else
						rRes.authorized = true;
				}
				else if (pObject == null && vQuery.Count() > 0)
				{
					rRes.authorized = true;
					rRes.reason = RnRBAuthorizationResultReason.USER_ROLES_ASSIGNED_OPERATION;
				}
			}


			return rRes;
		}

		private IEnumerable<DataRow> GetRoleOperations(int pRole)
		{
			var vSql = string.Format(
				"SELECT o.name AS Operation " +
				"FROM {0} r " +
				"	JOIN {1} ro on ro.role_id = r.id " +
				"	JOIN {2} o on o.id = ro.operation_id " +
				"WHERE r.id = @roleId", 
				tableRolesName, tableRoleOperationsName, tableOperationsName);
			var rSet = new DataSet();

			commonDataAdapter.SelectCommand = (SqlCommand)connection.CreateCommand();
			commonDataAdapter.SelectCommand.Parameters.AddWithValue("@roleId", pRole);
			commonDataAdapter.SelectCommand.CommandText = vSql;
			commonDataAdapter.Fill(rSet);
			commonDataAdapter.Dispose();
			
			return rSet.Tables[0].AsEnumerable();
		}

		private IEnumerable<DataRow> GetUserRoles(string pUser)
		{
			var vSql = string.Format(
				"SELECT r.id AS Role " +
				"FROM {0} r " +
				"	JOIN {1} ur ON ur.role_id = r.id " +
				"	JOIN {2} u ON u.id = ur.user_id " +
				"WHERE u.name = @userName",
				tableRolesName, tableUserRolesName, tableUsersName);
			var rSet = new DataSet();

			commonDataAdapter.SelectCommand = (SqlCommand)connection.CreateCommand();
			commonDataAdapter.SelectCommand.Parameters.AddWithValue("@userName", pUser);
			commonDataAdapter.SelectCommand.CommandText = vSql;
			commonDataAdapter.Fill(rSet);
			commonDataAdapter.Dispose();

			return rSet.Tables[0].AsEnumerable();
		}

		private IEnumerable<DataRow> GetOperationRelationshipTypes(string pOperation)
		{
			var vSql = string.Format(
				"SELECT rt.id AS RelationshipType, " +
				"		ort.role_id AS Role " +
				"FROM {0} rt " +
				"	JOIN {1} ort ON ort.relationship_type_id = rt.id " +
				"	JOIN {2} o ON o.id = ort.operation_id " +
				"WHERE o.name = @operationName",
				tableRelationshipTypesName, tableOperationRelationshipTypesName, tableOperationsName);
			var rSet = new DataSet();

			commonDataAdapter.SelectCommand = (SqlCommand)connection.CreateCommand();
			commonDataAdapter.SelectCommand.Parameters.AddWithValue("@operationName", pOperation);
			commonDataAdapter.SelectCommand.CommandText = vSql;
			commonDataAdapter.Fill(rSet);
			commonDataAdapter.Dispose();

			return rSet.Tables[0].AsEnumerable();
		}

		private IEnumerable<DataRow> GetUserRelationships(string pUser)
		{
			var vSql = string.Format(
				"SELECT u2.name AS RelatedUser, " +
				"		ur.relationship_type_id AS RelationshipType " +
				"FROM {0} u " +
				"	JOIN {1} ur ON ur.first_user_id = u.id " +
				"	JOIN {1} u2 ON u2.id = ur.second_user_id " +
				"WHERE u.name = @userName",
				tableUsersName, tableUserRelationshipsName, tableUsersName);
			var rSet = new DataSet();

			commonDataAdapter.SelectCommand = (SqlCommand)connection.CreateCommand();
			commonDataAdapter.SelectCommand.Parameters.AddWithValue("@userName", pUser);
			commonDataAdapter.SelectCommand.CommandText = vSql;
			commonDataAdapter.Fill(rSet);
			commonDataAdapter.Dispose();

			return rSet.Tables[0].AsEnumerable();
		}

		private DataRow GetObject(string pObject)
		{
			var vSql = string.Format(
				"SELECT u.name AS User " +
				"FROM {0} u " +
				"	JOIN {1} o ON o.user_id = u.id " +
				"WHERE o.name = @objectName",
				tableUsersName, tableObjectsName);
			var rSet = new DataSet();

			commonDataAdapter.SelectCommand = (SqlCommand)connection.CreateCommand();
			commonDataAdapter.SelectCommand.Parameters.AddWithValue("@objectName", pObject);
			commonDataAdapter.SelectCommand.CommandText = vSql;
			commonDataAdapter.Fill(rSet);
			commonDataAdapter.Dispose();

			return rSet.Tables[0].AsEnumerable().First();
		}

		/// <summary>
		/// Gets a token that hasn't expired for an object.
		/// </summary>
		/// <param name="pObject">The identifier of the object whose token to get.</param>
		/// <param name="pPurpose">The purpose of the token to get.</param>
		/// <returns>The token or <c>null</c>.</returns>
		/// <version>1.0</version>
		/// <since>1.0</since>
		public string GetNotExpiredObjectToken(string pObject, string pPurpose)
		{
			var vSql = string.Format(
				"SELECT ot.token AS Token " +
				"FROM {0} ot " +
				"WHERE ot.object = @objectName " +
				"	AND ot.purpose = @purposeName " +
				"	AND ot.expired = 0",
				tableObjectTokensName);
			var rSet = new DataSet();

			commonDataAdapter.SelectCommand = (SqlCommand)connection.CreateCommand();
			commonDataAdapter.SelectCommand.Parameters.AddWithValue("@objectName", pObject);
			commonDataAdapter.SelectCommand.Parameters.AddWithValue("@purposeName", pPurpose);
			commonDataAdapter.SelectCommand.CommandText = vSql;
			commonDataAdapter.Fill(rSet);
			commonDataAdapter.Dispose();

			var vEnum = rSet.Tables[0].AsEnumerable();

			return vEnum.Count() > 0 ? vEnum.First().Field<string>("Token") : null;
		}

		/// <summary>
		/// Adds a new token for an object.
		/// </summary>
		/// <param name="pObject">The identifier of the object whose token to add.</param>
		/// <param name="pPurpose">The purpose of the token to add.</param>
		/// <param name="pToken">The token to add.</param>
		/// <returns>Whether the insertion was successful or not.</returns>
		/// <version>1.0</version>
		/// <since>1.0</since>
		public bool AddObjectToken(string pObject, string pPurpose, object pToken)
		{
			var vSql = string.Format(
				"INSERT INTO {0} (object, purpose, token, expired) " +
				"		VALUES (@objectName, @purposeName, @token, 0) ",
				tableObjectTokensName);
			var vCmd = (SqlCommand)connection.CreateCommand();

			vCmd.CommandText = vSql;
			vCmd.Parameters.AddWithValue("@objectName", pObject);
			vCmd.Parameters.AddWithValue("@purposeName", pPurpose);
			vCmd.Parameters.AddWithValue("@token", pToken);
			connection.Open();
			var vRows = vCmd.ExecuteNonQuery();
			connection.Close();

			return vRows > 0;
		}

		/// <summary>
		/// Expires an object's token for a given purpose.
		/// </summary>
		/// <param name="pObject">The identifier of the object whose token to expire.</param>
		/// <param name="pPurpose">The purpose of the token to expire.</param>
		/// <version>1.0</version>
		/// <since>1.0</since>
		public void ExpireObjectToken(string pObject, string pPurpose)
		{
			var vSql = string.Format(
				"UPDATE {0} " +
				"	SET expired = 1" +
				"WHERE object = @objectName " +
				"	AND purpose = @purposeName ",
				tableObjectTokensName);
			var vCmd = (SqlCommand)connection.CreateCommand();

			vCmd.CommandText = vSql;
			vCmd.Parameters.AddWithValue("@objectName", pObject);
			vCmd.Parameters.AddWithValue("@purposeName", pPurpose);
			connection.Open();
			vCmd.ExecuteNonQuery();
			connection.Close();
		}

		/// <summary>
		/// Adds a new object to the access control storage.
		/// </summary>
		/// <param name="pObject">The identifier of the object to add.</param>
		/// <param name="pUser">The identifier of the user under whom to add the object.</param>
		/// <returns>Whether the insertion was successful or not.</returns>
		/// <version>1.0</version>
		/// <since>1.0</since>
		public bool AddObject(string pObject, int pUser)
		{
			var vSql = string.Format(
				"INSERT INTO {0} (name, user_id) " +
				"		VALUES (@objectName, @userId) ",
				tableObjectsName);
			var vCmd = (SqlCommand)connection.CreateCommand();

			vCmd.CommandText = vSql;
			vCmd.Parameters.AddWithValue("@objectName", pObject);
			vCmd.Parameters.AddWithValue("@userId", pUser);
			connection.Open();
			var vRows = vCmd.ExecuteNonQuery();
			connection.Close();

			return vRows > 0;
		}
	}
}
